﻿using System;
using System.Text;

namespace SkeinLib
{
    public static partial class SkeinCore
    {
        // all random number generator functions are broken out here for clarity.

        /// <summary>
        /// Initializes a specific class of Skein transforms, Random Number Generators, used for creating unique session keys,
        /// and in games.
        /// This generator will (theoritcally) not produce the same results twice.
        /// </summary>
        /// <param name="stateWordWidth">4, 8, or 16 words, for 256-, 512- and 1024-bit state widths, respectively.</param>
        /// <returns>The initialized and randomized state of the random number generator.</returns>
        public static UInt64[] InitializeRng(byte stateWordWidth)
        {
            if (stateWordWidth != 4 && stateWordWidth != 8 && stateWordWidth != 16)
                throw new ArgumentException("InitializeRng:  stateWordWidth must be length 4 (256bit), 8 (512bit), or 16 (1024bit)", "stateWordWidth");

            UInt64[] state = new UInt64[stateWordWidth];
            byte[] dummyB = new byte[Buffer.ByteLength(state)];
            Random thingy = new Random(DateTime.Now.Millisecond); 
            thingy.NextBytes(dummyB);
            ProcessGeneric(ref state, SKEIN_TYPE_KEY, dummyB); // first injection of pseudorandom as "key material"
            ProcessConfig(ref state, UInt64.MaxValue); // config with 'infinate' length
            string dummyS = "RNG:" + DateTime.Now.ToUniversalTime().ToString("o");
            ProcessPersonalization(ref state, ref dummyS); // next injection of pseudorandom
            thingy.NextBytes(dummyB);
            ProcessNonce(ref state, dummyB); // next injection of pseudorandom
            for (int i = 0; i < 32; i++)
            {
                dummyB = ProcessRngOutput(ref state, dummyB.Length);
                UpdateRng(ref state, dummyB); // feedback of first 32 "output blocks" back in as more pseudorandom
                // this is to scramble "start state"
            }
            return state;
        }

        /// <summary>
        /// Initializes a specific class of Skein transforms, Pseudo Random Number Generators, used for creating session keys, in stream ciphers,
        /// and in derived key streams.
        /// Pseudo Random Number Generators can reproduce the same set of random bytes when initialized with the same values, but the stream of
        /// bytes produced are not deterministic otherwise.
        /// </summary>
        /// <param name="stateWordWidth">4, 8, or 16 words, for 256-, 512- and 1024-bit state widths, respectively.</param>
        /// <param name="seedData">The seed from which the PRNG will be initialized.</param>
        /// <param name="nonce">[OPTIONAL] A nonce to tweak the PRNG in addition to the seed data.</param>
        /// <returns>The initialized state of the pseudo-random number generator.</returns>
        public static UInt64[] InitializePrng(byte stateWordWidth, byte[] seedData, byte[] nonce = null)
        {
            if (stateWordWidth != 4 && stateWordWidth != 8 && stateWordWidth != 16)
                throw new ArgumentException("InitializePrng:  stateWordWidth must be length 4 (256bit), 8 (512bit), or 16 (1024bit)", "stateWordWidth");
            if (nonce != null && nonce.Length < 1)
                throw new ArgumentException("InitializePrng:  nonce must have data if it is specified (length > 0)", "nonce");
            if (seedData == null || seedData.Length == 0)
                throw new ArgumentException("InitializePrng:  seedData must have data (length > 0) and cannot be null", "seedData");

            UInt64[] state = SkeinCore.Initialize(stateWordWidth, UInt64.MaxValue, seedData);
            string thingy = "PRNG";
            SkeinCore.ProcessPersonalization(ref state, ref thingy);
            if (nonce != null)
                SkeinCore.ProcessNonce(ref state, nonce);
            return state;
        }
        /// <summary>
        /// Generate a stream of random bytes.
        /// This can process bytes from either a PRNG or RNG transform.
        /// The state is updated after the execution of the method but is NOT destroyed.
        /// </summary>
        /// <param name="state">The state of the transform from which random bytes will be generated.</param>
        /// <param name="byteCount">The number of bytes requested from the random number generator.</param>
        /// <returns>Returns a stream of random bytes of the requested length.</returns>
        public static byte[] ProcessRngOutput(ref UInt64[] state, int byteCount)
        {
            Exception ex = ValidateState(state);
            if (ex != null) throw new ArgumentException("ProcessRngOutput:  state failed sanity check (see inner exception)", ex);
            if (byteCount < 1)
                throw new System.ArgumentOutOfRangeException("byteCount", "ProcessRngOutput:  byteCount cannot be negative or zero");

            UInt64[] tweak = { 8, SKEIN_FLAG_FIRST + SKEIN_TYPE_OUTPUT + SKEIN_FLAG_FINAL };

            int byteWidth = Buffer.ByteLength(state);
            UInt64[] transport = new UInt64[state.Length];

            //consume first block updating the persisted state
            switch (state.Length)
            {
                case 4: ThreeFish.ThreeFish256Internal(transport, state, tweak, true, ref state); break;
                case 8: ThreeFish.ThreeFish512Internal(transport, state, tweak, true, ref state); break;
                case 16: ThreeFish.ThreeFish1024Internal(transport, state, tweak, true, ref state); break;
            }

            //begin output of actual data
            byte[] result = new byte[byteCount];
            for (Int32 i = 0; (i * byteWidth) < byteCount; i++)
            {   //use the input array to the ThreeFish cipher for a counter, the state and tweak remain constant
                ClearWords(ref transport);
                transport[0] = (UInt64)(i + 1); //counter is i+1 because we already used 1 block to update the state above
                switch (state.Length)
                {
                    case 4: ThreeFish.ThreeFish256Internal(transport, state, tweak, true, ref transport); break;
                    case 8: ThreeFish.ThreeFish512Internal(transport, state, tweak, true, ref transport); break;
                    case 16: ThreeFish.ThreeFish1024Internal(transport, state, tweak, true, ref transport); break;
                }

                //dump the transport to the output, then discard
                int position = (i * byteWidth);
                if (position <= byteCount)
                    Buffer.BlockCopy(transport, 0, result, position, byteWidth);
                else
                    Buffer.BlockCopy(transport, 0, result, position, (int)(result.Length - position));
            }
            //zero out used memory
            ClearWords(ref transport, true);
            ClearWords(ref tweak, true);
            //END OUTPUT

            return result;
        }

        /// <summary>
        /// Update a Random Number transform with new data.
        /// This can update either a PRNG or RNG transform.
        /// </summary>
        /// <param name="state">The state of the transform into which the new data will be injected.</param>
        /// <param name="data">The data to be injected as an array of bytes.</param>
        public static void UpdateRng(ref UInt64[] state, byte[] data)
        {
            Exception ex = ValidateState(state);
            if (ex != null)
                throw new ArgumentException("UpdateRNG:  state failed sanity check (see inner exception)", ex);
            if (data == null || data.Length < 1)
                throw new ArgumentException("UpdateRNG:  data cannot be null or empty", "data");

            ProcessGeneric(ref state, SKEIN_TYPE_MSG, data);

            //BEGIN OUTPUT
            // Why the 8?  I have no idea, but I'm assuming that it means 8 bytes (64 bits)
            // It is not explained in the white paper
            UInt64[] tweak = { 8, SKEIN_FLAG_FIRST + SKEIN_TYPE_OUTPUT + SKEIN_FLAG_FINAL };
            UInt64[] transport = new UInt64[state.Length];            
            switch (state.Length)
            {
                case 4: ThreeFish.ThreeFish256Internal(transport, state, tweak, true, ref state); break;
                case 8: ThreeFish.ThreeFish512Internal(transport, state, tweak, true, ref state); break;
                case 16: ThreeFish.ThreeFish1024Internal(transport, state, tweak, true, ref state); break;
            }
            //zero out used memory
            ClearWords(ref transport, true);
            ClearWords(ref tweak, true);
            //END OUTPUT
        }
    }//end class
}
